/*

Intro:

    For some unknown reason most of our developers left
    the company. We need to actively hire now.
    In the media we've read that companies that invent
    and publish new technologies attract more potential
    candidates. We need to use this opportunity and
    invent and publish some npm packages. Following the
    new trend of functional programming in JS we
    decided to develop a functional utility library.
    This will put us on the bleading edge since we are
    pretty much sure no one else did anything similar.
    We also provided some jsdoc along with the
    functions, but it might sometimes be inaccurate.

Exercise:

    Provide proper typing for the specified functions.

Bonus:

    Could you please also refactor the code to reduce
    code duplication?
    You might need some excessive type casting to make
    it really short.

*/

/*
这里其实大部分都是函数柯理化的示例
*/

function toFunctional<T extends Function> (func: T): Function {
  // func是一个函数类型, func.length表示参数的长度. 以map为例这里:func的参数为fn和input，因此length为2
  const fullArgCount = func.length
  // curriedArgs 闭包保存之前传入的参数数组.
  function createSubFunction (curriedArgs: unknown[]) {
    // curriedArgs是map接收的参数: map()表示curriedArgs为空数组
    return function (this: unknown) {
      const newCurriedArguments = curriedArgs.concat(Array.from(arguments))
      if (newCurriedArguments.length > fullArgCount) {
        throw new Error('Too many arguments')
      }
      // 如果传参是两个,即fn和input都传了
      if (newCurriedArguments.length === fullArgCount) {
        return func.apply(this, newCurriedArguments)
      }
      // map(func)或者map()的情况
      return createSubFunction(newCurriedArguments)
    }
  }
  return createSubFunction([])
}

// mapper函数类型 I表示Input的泛型，O表示Output的泛型
interface MapperFunc<I, O> {
  (): MapperFunc<I, O>;
  (input: I[]): O[];
}

interface MapFunc {
  (): MapFunc;
  <I, O>(mapper: (item: I) => O): MapperFunc<I, O>;
  <I, O>(mapper: (item: I) => O, input: I[]): O[];
}

/**
 * 2 arguments passed: returns a new array
 * which is a result of input being mapped using
 * the specified mapper.
 *
 * 1 argument passed: returns a function which accepts
 * an input and returns a new array which is a result
 * of input being mapped using original mapper.
 *
 * 0 arguments passed: returns itself.
 */
export const map = toFunctional(<I, O>(fn: (arg: I) => O, input: I[]) =>
  input.map(fn)
) as MapFunc

console.log(map())
console.log(map((item:string|number) => item + '123'))
console.log(map((item:string) => item + '123')(['a', 'b', 'c']))
console.log(map(item => item + '123', [1, 2, 3, 4, 5]))

interface FiltererFunc<I> {
  (): FiltererFunc<I>;
  (input: I[]): I[];
}

interface FilterFunc {
  (): FilterFunc;
  <I>(filterer: (item: I) => boolean): FiltererFunc<I>;
  <I>(filterer: (item: I) => boolean, input: I[]): I[];
}

/**
 * 2 arguments passed: returns a new array
 * which is a result of input being filtered using
 * the specified filter function.
 *
 * 1 argument passed: returns a function which accepts
 * an input and returns a new array which is a result
 * of input being filtered using original filter
 * function.
 *
 * 0 arguments passed: returns itself.
 */
export const filter = toFunctional(<I>(fn: (item: I) => boolean, input: I[]) =>
  input.filter(fn)
) as FilterFunc

// 这个是reduce(fn,initial)返回的函数的类型定义
interface ReducerInitialFunc<I, O> {
  (): ReducerInitialFunc<I, O>;
  (input: I[]): O;
}

// 这个是reduce（fn)返回的函数的类型定义，可以接受一个参数:initialValue, 或者两个参数:initial和input
interface ReducerFunc<I, O> {
  (): ReducerFunc<I, O>;
  (initialValue: O): ReducerInitialFunc<I, O>;
  (initialValue: O, input: I[]): O;
}

// 这是reduce()的类型声明，可以不接受参数或者接受1~3个参数，对应给次的返回类型
interface ReduceFunc {
  (): ReduceFunc;
  <I, O>(reducer: (acc: O, val: I) => O): ReducerFunc<I, O>;
  <I, O>(reducer: (acc: O, val: I) => O, initialValue: O): ReducerInitialFunc<I, O>;
  <I, O>(reducer: (acc: O, val: I) => O, initialValue: O, input: I[]): O;
}

/**
 * 3 arguments passed: reduces input array it using the
 * specified reducer and initial value and returns
 * the result.
 *
 * 2 arguments passed: returns a function which accepts
 * input array and reduces it using previously specified
 * reducer and initial value and returns the result.
 *
 * 1 argument passed: returns a function which:
 *   * when 2 arguments is passed to the subfunction, it
 *     reduces the input array using specified initial
 *     value and previously specified reducer and returns
 *     the result.
 *   * when 1 argument is passed to the subfunction, it
 *     returns a function which expects the input array
 *     and reduces the specified input array using
 *     previously specified reducer and inital value.
 *   * when 0 argument is passed to the subfunction, it
 *     returns itself.
 *
 * 0 arguments passed: returns itself.
 */
export const reduce = toFunctional(
  <I, O>(reducer: (acc: O, item: I) => O, initialValue: O, input: I[]) =>
    input.reduce(reducer, initialValue)
) as ReduceFunc

interface ArithmeticArgFunc {
  (): ArithmeticArgFunc;
  (b: number): number;
}

interface ArithmeticFunc {
  (): ArithmeticFunc;
  (a: number): ArithmeticArgFunc;
  (a: number, b: number): number;
}

/**
 * 2 arguments passed: returns sum of a and b.
 *
 * 1 argument passed: returns a function which expects
 * b and returns sum of a and b.
 *
 * 0 arguments passed: returns itself.
 */
export const add = toFunctional(
  (a: number, b: number) => a + b
) as ArithmeticFunc

/**
 * 2 arguments passed: subtracts b from a and
 * returns the result.
 *
 * 1 argument passed: returns a function which expects
 * b and subtracts b from a and returns the result.
 *
 * 0 arguments passed: returns itself.
 */
export const subtract = toFunctional(
  (a: number, b: number) => a - b
) as ArithmeticFunc

/*
这下面的prop函数改写有问题，原js版的事prop(obj,propName)的调用方法，结果这里改成了prop(propName,obj)
 */
interface PropNameFunc<K extends string> {
  (): PropNameFunc<K>;
  // 这里泛型约束可以理解下，O是一个 类型为 属性名包含K，属性值为O[K]的泛型
  <O extends { [key in K]: O[K] }>(obj: O): O[K];
}

interface PropFunc {
  (): PropFunc;
  <K extends string>(propName: K): PropNameFunc<K>;
  <O, K extends keyof O>(propName: K, obj: O): O[K];
}

/**
 * 2 arguments passed: returns value of property
 * propName of the specified object.
 *
 * 1 argument passed: returns a function which expects
 * propName and returns value of property propName
 * of the specified object.
 *
 * 0 arguments passed: returns itself.
 */
export const prop = toFunctional(
  <O, K extends keyof O>(obj: O, propName: K): O[K] => obj[propName]
) as PropFunc


type F<A extends unknown[], R> = (...args: A) => R;
type TR<I, O> = (arg: I) => O;

interface PipeFunc {
  (): PipeFunc;
  <A1 extends unknown[], R1>(f: F<A1, R1>): (...args: A1) => R1;
  <A1 extends unknown[], R1, R2>(f: F<A1, R1>, tr1: TR<R1, R2>): (
    ...args: A1
  ) => R2;
  <A1 extends unknown[], R1, R2, R3>(
    f: F<A1, R1>,
    tr1: TR<R1, R2>,
    tr2: TR<R2, R3>
  ): (...args: A1) => R3;
  <A1 extends unknown[], R1, R2, R3, R4>(
    f: F<A1, R1>,
    tr1: TR<R1, R2>,
    tr2: TR<R2, R3>,
    tr3: TR<R3, R4>
  ): (...args: A1) => R4;
  <A1 extends unknown[], R1, R2, R3, R4, R5>(
    f: F<A1, R1>,
    tr1: TR<R1, R2>,
    tr2: TR<R2, R3>,
    tr3: TR<R3, R4>,
    tr4: TR<R4, R5>
  ): (...args: A1) => R5;
}

/**
 * >0 arguments passed: expects each argument to be
 * a function. Returns a function which accepts the
 * same arguments as the first function. Passes these
 * arguments to the first function, the result of
 * the first function passes to the second function,
 * the result of the second function to the third
 * function... and so on. Returns the result of the
 * last function execution.
 *
 * 0 arguments passed: returns itself.
 */
export const pipe: PipeFunc = function (...functions: Function[]) {
  if (arguments.length === 0) {
    return pipe
  }
  return function subFunction () {
    let nextArguments = Array.from(arguments)
    let result
    for (const func of functions) {
      result = func(...nextArguments)
      nextArguments = [result]
    }
    return result
  }
}
console.log(pipe(add(3))(2))
console.log(pipe(add(3), subtract(1))(2))
console.log(pipe(filter((item:number) => item > 3), map(item => item + 'abc'))([1, 2, 3, 4, 5, 6, 7]))
// 这个类型声明不太行，最多支持5个函数
// console.log(pipe(add(3), subtract(1), add(1),add(1),add(1),add(1))(2))
